﻿//-----------------------------------------------------------------------
// <copyright file="MultipleMessagesDispatcher.cs" company="OOHM PROCESSAMENTO DE DADOS LTDA.">
//     Microsoft Public License (MS-PL) This license governs use of the accompanying
//     software. If you use the software, you accept this license. If you do not
//     accept the license, do not use the software. 1. Definitions The terms "reproduce,"
//     "reproduction," "derivative works," and "distribution" have the same meaning
//     here as under U.S. copyright law. A "contribution" is the original software,
//     or any additions or changes to the software. A "contributor" is any person
//     that distributes its contribution under this license. "Licensed patents"
//     are a contributor's patent claims that read directly on its contribution.
//     2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license,
//     including the license conditions and limitations in section 3, each contributor
//     grants you a non-exclusive, worldwide, royalty-free copyright license to
//     reproduce its contribution, prepare derivative works of its contribution,
//     and distribute its contribution or any derivative works that you create.
//     (B) Patent Grant- Subject to the terms of this license, including the license
//     conditions and limitations in section 3, each contributor grants you a non-exclusive,
//     worldwide, royalty-free license under its licensed patents to make, have
//     made, use, sell, offer for sale, import, and/or otherwise dispose of its
//     contribution in the software or derivative works of the contribution in
//     the software. 3. Conditions and Limitations (A) No Trademark License- This
//     license does not grant you rights to use any contributors' name, logo, or
//     trademarks. (B) If you bring a patent claim against any contributor over
//     patents that you claim are infringed by the software, your patent license
//     from such contributor to the software ends automatically. (C) If you distribute
//     any portion of the software, you must retain all copyright, patent, trademark,
//     and attribution notices that are present in the software. (D) If you distribute
//     any portion of the software in source code form, you may do so only under
//     this license by including a complete copy of this license with your distribution.
//     If you distribute any portion of the software in compiled or object code
//     form, you may only do so under a license that complies with this license.
//     (E) The software is licensed "as-is." You bear the risk of using it. The
//     contributors give no express warranties, guarantees or conditions. You may
//     have additional consumer rights under your local laws which this license
//     cannot change. To the extent permitted under your local laws, the contributors
//     exclude the implied warranties of merchantability, fitness for a particular
// </copyright>
//-----------------------------------------------------------------------
namespace AbstractSaaSTemplate.Robots.Jobs.Tasks{    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using AbstractSaaSTemplate.Domain.Models.Entities;
    using AbstractSaaSTemplate.Domain.Repositories;
    using AbstractSaaSTemplate.Kernel;

    /// <summary>
    /// Represents a multiple messages dispatcher job
    /// that implements the 'TaskBase' defined by the
    /// kernel of the application.
    /// </summary>
    public sealed class MultipleMessagesDispatcher : TaskBase
    {
        /// <summary>
        /// Defines the max number of slots available this will limit the amount of messages that can be processed at once.
        /// Much easier to DEBUG if you use 1, instead of many.
        /// </summary>
        private const int MaxSlots = 1;
        
        /// <summary>
        /// Initializes a new instance of the MultipleMessagesDispatcher class.
        /// </summary>
        /// <param name="repositoryForMessages">The repository to operate 'Messages'.</param>
        /// <param name="repositoryForMessaging">The repository to operate 'Messaging'.</param>
        public MultipleMessagesDispatcher(
            IMessagesRepository repositoryForMessages,
            IMessagingRepository repositoryForMessaging)
        {
            this.RepositoryForMessages = repositoryForMessages;
            this.RepositoryForMessaging = repositoryForMessaging;

            // we here initialize the number of slots that will be available
            this.DispatcherSlots = new SlotInfo[MaxSlots];
        }

        /// <summary>
        /// Gets the repository to operate 'Messages'.
        /// </summary>
        public IMessagesRepository RepositoryForMessages { get; private set; }

        /// <summary>
        /// Gets the repository to operate 'Messaging'.
        /// </summary>
        public IMessagingRepository RepositoryForMessaging { get; private set; }

        /// <summary>
        /// Gets or sets all slots of messages available.
        /// </summary>
        private IList<SlotInfo> DispatcherSlots { get; set; }

        /// <summary>
        /// Performs the tasks for which the class was designed.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Reliability",
            "CA2000:Dispose objects before losing scope",
            Justification = "Dispose is invoked assynchronously.")]
        protected override void Perform()
        {
            // we load a list of messages that will be invisible during a few seconds
            // this is done so that another instance of this service will not attempt
            // to duplicate send the same message.
            const int TimeInSecondsThatMessagesWillBeInvisibleAfterQuery = 30;

            // first we clean up all slots that have been taken released
            var slotsToRelease = this.DispatcherSlots.Where(d => d != null && d.ReadyForCleanUp);
            this.ReleaseSlots(slotsToRelease);

            var slotsTaken = this.DispatcherSlots.Where(d => d != null).Count();
            var amountOfMessagesToProcess = MaxSlots - slotsTaken;
            
            // now we retrieve the number of messages that we want to process
            // it is the diference of MaxSlots and the number 
            var messages = this.RepositoryForMessages.GetMessagesReadyToDispatch(
                amountOfMessagesToProcess,
                TimeInSecondsThatMessagesWillBeInvisibleAfterQuery).ToArray();

            // to avoid multi-threading problems we lock it in order to keep
            // it thread safe. we will now include all dispatching threads for it.
            lock (this.DispatcherSlots)
            {
                // defines the index of the message that is being consumed.
                var messagesIndex = default(int);

                for (var slotIndex = 0; slotIndex < this.DispatcherSlots.Count; slotIndex++)
                {
                    if (messagesIndex >= messages.Count())
                    {
                        break;
                    }

                    var slotInfo = this.DispatcherSlots[slotIndex];
                    if (slotInfo == null)
                    {
                        var worker = new BackgroundWorker();
                        worker.WorkerReportsProgress = false;
                        worker.WorkerSupportsCancellation = false;
                        worker.DoWork += new DoWorkEventHandler(this.DispatherWorker_DoWork);
                        worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(this.DispatherWorker_RunWorkerCompleted);
                        
                        slotInfo = new SlotInfo(
                            slotIndex,
                            worker,
                            messages[messagesIndex++]);

                        slotInfo.Worker.RunWorkerAsync(slotInfo);
                        this.DispatcherSlots[slotIndex] = slotInfo;
                    }
                }
            }
        }

        /// <summary>
        /// Release all slots defined.
        /// </summary>
        /// <param name="slotsToRelease">A list of all slots that are ready to be fred.</param>
        private void ReleaseSlots(IEnumerable<SlotInfo> slotsToRelease)
        {
            if (slotsToRelease == null || slotsToRelease.Count() == 0)
            {
                return;
            }

            lock (this.DispatcherSlots)
            {
                foreach (var slotToRelease in slotsToRelease)
                {
                    // first we free all the resources from
                    // the background worker.
                    if (slotToRelease.Worker != null)
                    {
                        slotToRelease.Worker.Dispose();
                    }

                    // then we mark the position of the slot as null
                    // so that the service can load another SlotInfo
                    // into it.
                    this.DispatcherSlots[slotToRelease.Index] = null;
                }
            }
        }

        /// <summary>
        /// Performs the assync work for the Background Worker.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The event args of the event.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Design",
            "CA1031:DoNotCatchGeneralExceptionTypes",
            Justification = "We want to report back any exception and keep the execution safe of this mult-thread bit.")]
        private void DispatherWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            var workerData = (SlotInfo) e.Argument;
            e.Result = workerData;
            try
            {
                this.RepositoryForMessaging.SendMessage(workerData.Message);
                this.RepositoryForMessages.MarkMessageAsDispatched(workerData.Message.Id);
            }
            catch (Exception ex)
            {
                var eventArgs = new TaskErrorEventArgs(
                    int.MaxValue,
                    ex.Message,
                    ex);

                this.OnTaskError(eventArgs);
            }
        }

        /// <summary>
        /// Performs the message dipatching.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The event args of the event.</param>
        private void DispatherWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            lock (this.DispatcherSlots)
            {
                var workerData = (SlotInfo) e.Result;
                var indexToRemove = workerData.Index;

                // here we mark slot to be unloaded.
                workerData.ReadyForCleanUp = true;
            }
        }

        /// <summary>
        /// Defines the worker data that will be passed onto the worker.
        /// </summary>
        private class SlotInfo
        {
            /// <summary>
            /// Initializes a new instance of the SlotInfo class.
            /// </summary>
            /// <param name="index">The index of the slot.</param>
            /// <param name="worker">The background worker that will perform the async job.</param>
            /// <param name="message">The message that will be processed.</param>
            public SlotInfo(
                int index,
                BackgroundWorker worker,
                Message message)
            {
                this.Index = index;
                this.Worker = worker;
                this.Message = message;
                this.ReadyForCleanUp = false;
            }

            /// <summary>
            /// Gets the worker that is going to perform the job.
            /// </summary>
            public BackgroundWorker Worker { get; private set; }
            
            /// <summary>
            /// Gets the index of the slot related to this dispatching process.
            /// </summary>
            public int Index { get; private set; }

            /// <summary>
            /// Gets the instance of message that will be processed.
            /// </summary>
            public Message Message { get; private set; }

            /// <summary>
            /// Gets or sets a value indicating whether the slot is ready to be cleaned up.
            /// </summary>
            public bool ReadyForCleanUp { get; set; }
        }
    }
}


